package com.wangdong.wdutils.logger;

import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.text.TextUtils;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import static com.wangdong.wdutils.logger.DLLogger.ASSERT;
import static com.wangdong.wdutils.logger.DLLogger.DEBUG;
import static com.wangdong.wdutils.logger.DLLogger.ERROR;
import static com.wangdong.wdutils.logger.DLLogger.INFO;
import static com.wangdong.wdutils.logger.DLLogger.VERBOSE;
import static com.wangdong.wdutils.logger.DLLogger.WARN;


/**
 * CSV formatted file logging for Android.
 * Writes to CSV the following data:
 * epoch timestamp, ISO8601 timestamp (human-readable), log level, tag, log message.
 */
public class DLCsvDLFormatStrategy implements DLFormatStrategy {

  private static final String NEW_LINE = System.getProperty("line.separator");
  /**
   * Android's max limit for a log entry is ~4076 bytes,
   * so 4000 bytes is used as chunk size since default charset
   * is UTF-8
   */
  private static final int CHUNK_SIZE = 4000;

  /**
   * The minimum stack trace index, starts at this class after two native calls.
   */
  private static final int MIN_STACK_OFFSET = 5;
  /**
   * Drawing toolbox
   */
  private static final char TOP_LEFT_CORNER = '┌';
  private static final char BOTTOM_LEFT_CORNER = '└';
  private static final char MIDDLE_CORNER = '├';
  private static final char HORIZONTAL_LINE = '│';
  private static final String DOUBLE_DIVIDER = "────────────────────────────────────────────────────────";
  private static final String SINGLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄";
  private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER ;
  private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER ;
  private static final String MIDDLE_BORDER = MIDDLE_CORNER + SINGLE_DIVIDER ;

  private final Date date;
  private final SimpleDateFormat dateFormat;
  private final DLLogStrategy logStrategy;
  private final String tag;
  private StringBuilder buffer = null;

  private DLCsvDLFormatStrategy(Builder builder) {
    date = builder.date;
    dateFormat = builder.dateFormat;
    logStrategy = builder.logStrategy;
    tag = builder.tag;
  }

  public static Builder newBuilder() {
    return new Builder();
  }

  @Override
  public void log(int priority, String onceOnlyTag, String message) {
    String tag = formatTag(onceOnlyTag);

    if (buffer!=null){
      buffer= null;
    }
    buffer = new StringBuilder();

    date.setTime(System.currentTimeMillis());

    logTopBorder(priority, tag);
    logChunk(priority,tag,HORIZONTAL_LINE + " "+dateFormat.format(date));
    logDivider(priority,tag);
    logHeaderContent(priority, tag, 3);

    //get bytes of message with system's default charset (which is UTF-8 for Android)
    byte[] bytes = message.getBytes();
    int length = bytes.length;
    if (length <= CHUNK_SIZE) {

      logDivider(priority, tag);

      logContent(priority, tag, message);
      logBottomBorder(priority, tag);
    }else {

      logDivider(priority, tag);

      for (int i = 0; i < length; i += CHUNK_SIZE) {
        int count = Math.min(length - i, CHUNK_SIZE);
        //create a new String with system's default charset (which is UTF-8 for Android)
        logContent(priority, tag, new String(bytes, i, count));
      }
      logBottomBorder(priority, tag);
    }

    logChunk(priority,tag,NEW_LINE);

    logStrategy.log(priority, tag, buffer.toString());
  }


  private void logTopBorder(int logType, String tag) {
    logChunk(logType, tag, TOP_BORDER);
  }

  @SuppressWarnings("StringBufferReplaceableByString")
  private void logHeaderContent(int logType, String tag, int methodCount) {
    StackTraceElement[] trace = Thread.currentThread().getStackTrace();

    logChunk(logType, tag, HORIZONTAL_LINE + " Thread: " + Thread.currentThread().getName()+"---"+logLevel(logType)+"---"+tag);
    logDivider(logType, tag);

    String level = "";

    int stackOffset = getStackOffset(trace);

    //corresponding method count with the current stack may exceeds the stack trace. Trims the count
    if (methodCount + stackOffset > trace.length) {
      methodCount = trace.length - stackOffset - 1;
    }

    for (int i = methodCount+2; i > 2; i--) {
      int stackIndex = i + stackOffset;
      if (stackIndex >= trace.length) {
        continue;
      }
      StringBuilder builder = new StringBuilder();
      builder.append(HORIZONTAL_LINE)
              .append(' ')
              .append(level)
              .append(getSimpleClassName(trace[stackIndex].getClassName()))
              .append(".")
              .append(trace[stackIndex].getMethodName())
              .append(" ")
              .append(" (")
              .append(trace[stackIndex].getFileName())
              .append(":")
              .append(trace[stackIndex].getLineNumber())
              .append(")");
      level += "   ";
      logChunk(logType, tag, builder.toString());
    }
  }

  private void logBottomBorder(int logType, String tag) {
    logChunk(logType, tag, BOTTOM_BORDER);
  }

  private void logDivider(int logType, String tag) {
    logChunk(logType, tag, MIDDLE_BORDER);
  }

  private void logContent(int logType, String tag, String chunk) {
    String[] lines = chunk.split(System.getProperty("line.separator"));
    for (String line : lines) {
      logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
    }
  }

  private void logChunk(int priority, String tag, String chunk) {
//      logStrategy.log(priority, tag, chunk);
      if (buffer!=null){
          buffer.append(chunk);
          buffer.append("\r\n");
      }
  }

  private String getSimpleClassName(String name) {
    int lastIndex = name.lastIndexOf(".");
    return name.substring(lastIndex + 1);
  }

  /**
   * Determines the starting index of the stack trace, after method calls made by this class.
   *
   * @param trace the stack trace
   * @return the stack offset
   */
  private int getStackOffset(StackTraceElement[] trace) {
    for (int i = MIN_STACK_OFFSET; i < trace.length; i++) {
      StackTraceElement e = trace[i];
      String name = e.getClassName();
      if (!name.equals(DLLoggerPrinter.class.getName()) && !name.equals(DLLogger.class.getName())) {
        return --i;
      }
    }
    return -1;
  }

  private String formatTag(String tag) {
    if (!TextUtils.isEmpty(tag) && !equals(this.tag, tag)) {
      return this.tag + "-" + tag;
    }
    return this.tag;
  }

  /**
   * Returns true if a and b are equal, including if they are both null.
   * <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
   * both the arguments were instances of String.</i></p>
   *
   * @param a first CharSequence to check
   * @param b second CharSequence to check
   * @return true if a and b are equal
   * <p>
   * NOTE: Logic slightly change due to strict policy on CI -
   * "Inner assignments should be avoided"
   */
  private boolean equals(CharSequence a, CharSequence b) {
    if (a == b) return true;
    if (a != null && b != null) {
      int length = a.length();
      if (length == b.length()) {
        if (a instanceof String && b instanceof String) {
          return a.equals(b);
        } else {
          for (int i = 0; i < length; i++) {
            if (a.charAt(i) != b.charAt(i)) return false;
          }
          return true;
        }
      }
    }
    return false;
  }


  private String logLevel(int value) {
    switch (value) {
      case VERBOSE:
        return "VERBOSE";
      case DEBUG:
        return "DEBUG";
      case INFO:
        return "INFO";
      case WARN:
        return "WARN";
      case ERROR:
        return "ERROR";
      case ASSERT:
        return "ASSERT";
      default:
        return "UNKNOWN";
    }
  }

  public static final class Builder {
    private static final int MAX_BYTES = 500 * 1024; // 500K averages to a 4000 lines per file

    private Date date;
    private SimpleDateFormat dateFormat;
    private DLLogStrategy logStrategy;
    private String tag = "PRETTY_LOGGER";
    private String saveFolder ;

    private Builder() {
    }

    public Builder date(Date val) {
      date = val;
      return this;
    }

    public Builder dateFormat(SimpleDateFormat val) {
      dateFormat = val;
      return this;
    }

    public Builder logStrategy(DLLogStrategy val) {
      logStrategy = val;
      return this;
    }

    public Builder tag(String tag) {
      this.tag = tag;
      return this;
    }

    public Builder saveFolder(String saveFolder){
      this.saveFolder = saveFolder;
      return this;
    }

    public DLCsvDLFormatStrategy build() {
      if (date == null) {
        date = new Date();
      }
      if (dateFormat == null) {
        dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.UK);
      }
      if (logStrategy == null) {

        if (saveFolder==null){
          String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
          saveFolder = diskPath + File.separatorChar + "logger" + File.separatorChar;
        }

        HandlerThread ht = new HandlerThread("AndroidFileLogger." + saveFolder);
        ht.start();
        Handler handler = new DLDiskLogStrategy.WriteHandler(ht.getLooper(), saveFolder, MAX_BYTES);
        logStrategy = new DLDiskLogStrategy(handler);
      }
      return new DLCsvDLFormatStrategy(this);
    }
  }
}
